"""
Stack is a linear data structure that follows the LIFO (Last In, First Out) principle.

It provides an ordered collection of elements where the addition and removal of items take place at the same end,
commonly referred to as the "top" of the stack. The most recently added item is always at the top, and the oldest
item in the stack is at the bottom.
"""

class Node:
    """
    Node is a basic unit of a stack, which holds the data and a reference to the next node.

    Attributes:
        data: Data stored in node.
        next: Next node in stack.
    """

    def __init__(self, data=None):
        """
        Initialize a node with data and set next to None.

        Args:
            data: Initial data for this node. Defaults to None.
        """
        self.data = data
        self.next = None


class Stack:
    """
    A Stack follows LIFO (Last In First Out) principle.

    Attributes:
        top: The top node in the stack.
        size: The number of elements in the stack.
    """

    def __init__(self):
        """
        Initialize an empty stack.
        """
        self.top = None
        self.size = 0

    def push(self, data):
        """
        Pushes an item onto the top of this stack.

        Args:
            data: Data to be pushed onto stack.
        """
        new_node = Node(data)
        if self.top:
            new_node.next = self.top
        self.top = new_node
        self.size += 1

    def pop(self):
        """
        Removes the object at the top of this stack and returns that object as the value of this function.

        Returns:
            The data from the top of stack. If stack is empty, returns None.
        """
        if self.top:
            data = self.top.data
            self.top = self.top.next
            self.size -= 1
            return data
        return None

    def peek(self):
        """
        Looks at the object at the top of this stack without removing it from the stack.

        Returns:
            The data from the top of stack. If stack is empty, returns None.
        """
        if self.top:
            return self.top.data
        return None

    def is_empty(self):
        """
        Tests if this stack is empty.

        Returns:
            True if and only if this stack contains no items; False otherwise.
        """
        return self.top is None

    def __len__(self):
        """
        Returns the number of elements in this stack.

        Returns:
            The number of elements in this stack.
        """
        return self.size

    def __repr__(self):
        """
        Returns a string that represents a valid Python expression that could be used to recreate this stack.

        Returns:
            A string that represents a valid Python expression.
        """
        return f'Stack(size={self.size}, top={self.peek()})'

    def __str__(self):
        """
        Returns a string representing the stack. The string representation consists of the stack's elements and the words 'Top:' and 'Bottom', enclosed in square brackets ("[]"). Adjacent elements are separated by the characters "->" (a space, a hyphen, and a space).

        Returns:
            A string representing the stack.
        """
        node = self.top
        result = ['Top:']
        while node:
            result.append(str(node.data))
            node = node.next
        result.append('Bottom')
        return ' -> '.join(result)

if __name__ == "__main__":
    stack = Stack()

    # push data onto the stack
    for i in range(5):
        stack.push(i)

    # print the stack
    print(f"Stack: {stack}")  # Stack: Top: -> 4 -> 3 -> 2 -> 1 -> 0 -> Bottom

    # print the stack representation
    print(repr(stack))  # Stack(size=5, top=4)

    # peek at the top of the stack
    print(f"Peek: {stack.peek()}")  # Peek: 4

    # pop data from the stack
    print(f"Pop: {stack.pop()}")  # Pop: 4

    # print the stack after popping
    print(f"Stack after pop: {stack}")  # Stack after pop: Top: -> 3 -> 2 -> 1 -> 0 -> Bottom

    # check if the stack is empty
    print(f"Is stack empty: {stack.is_empty()}")  # Is stack empty: False

    # get the number of elements in the stack using len()
    print(f"Stack size: {len(stack)}")  # Stack size: 4
